import os
import wx
import re
import sys
import subprocess
from literate import Literate
from easier_frame import MyEasierFrame
from easier_mgf_builders import build_mgf_from_dtas
from easier_mgf_builders import add_quant_to_cid, build_mgf
from easier_mgf_builders import build_mgfs_from_mzxml
from easier_mgf_builders import get_file_handles
#
#
#
class MyFileDropTarget(wx.FileDropTarget):
    ""
    def __init__(self, window):
        wx.FileDropTarget.__init__(self)
        self.window = window

    def OnDropFiles(self, x, y, filenames):
        self.window.notify(filenames)
#
#
#
class TextCtrlLogger():
    ""
    def __init__(self, widget, mode='a'):
        self.widget = widget
        self.mode = mode
    
    def write(self, text):
        if self.mode == 'a':
            self.widget.AppendText(text)
        elif self.mode == 'w':
            self.widget.SetValue(text)
#
#
#
class ReAdW():
    
    EXE = "exe_readw/ReAdW.exe"
    
    def __init__(self, panel, logger):
        self.panel = panel
        self.logger = logger if logger else sys.stdout
        self.temp = 'readw_temp'
        self.keep = False
        self.mzml = False
        self.get_options()
    
    def get_options(self):
        """Usage: ReAdW [options] <raw file path> [<output file>]
        Options (--option)
        mzXML:  mzXML mode (default)
        mzML:   mzML mode (will use msconvert)
                one of --mzXML or --mzML must be selected
        centroid, -c: Centroid all scans (MS1 and MS2)
                meaningful only if data was acquired in profile mode;
                default: off
        precursorFromFilterLine:  [Advanced option, default OFF] only
                try to get the precursor MZ value from the Thermo
                "filterline" text; only use this if you have a good reason!
                Otherwise, the program first will try to obtain a more accurate
                mass from the "Monoisotopic M/Z:" "trailer value"
        compress, -z: Use zlib for compressing peaks
                default: off
        verbose, -v:   verbose
        gzip, -g:   gzip the output file (independent of peak compression)

        output file: (Optional) Filename for output file;
        if not supplied, the output file will be created
        in the same directory as the input file.
        """
        
        options = []
                    
        for option, values in self.panel.readw.hints.iteritems():
            
            (name, hint) = values[0:2]
            
            try:
                widget = getattr(self.panel.readw, name)
            except AttributeError:
                continue
            
            if name.startswith('ckbx'):
                if widget.IsChecked(): options.append('--%s' %option)
            elif name.startswith('tc'):
                value = widget.GetValue()
                if value: options.append('--%s %s' %(option, value))
            else:
                value = widget.GetValue()
                setattr(self, option, value)
                
        self.options = ' '.join(options)
            
    def execute(self):
        "call converter executable"
        if not os.path.exists(self.temp):
            os.mkdir(self.temp)
            
        frmt = '--mzML' if self.mzml else '--mzXML' 
            
        for raw in self.panel.infiles:
            basename = os.path.basename(raw)
            (basename, ext) = os.path.splitext(basename)
            basename = basename + '.mzXML' 
            output = os.path.join(os.getcwd(), self.temp, basename) 
            command = "%s %s %s %s %s" % (self.EXE, frmt,
                                          self.options, raw, output)
            self.logger.write(command + '\n')
            process = subprocess.Popen(command,
                                             stdout=subprocess.PIPE,
                                             stderr=subprocess.PIPE,
                                             shell= False)
                                            
            self.logger.write(process.stdout.read())
            self.logger.write(process.stderr.read())
            
    def postprocess(self, remove_z_one, split_2_3, add_hcd):
        "converts mzXML from ReAdW in custom mgfs"
        if self.mzml: return
        for xml in os.listdir(self.temp):
            if xml.endswith('mzXML'):
                (xmlname, ext) = os.path.splitext(xml)
                outfile = xmlname + '.mgf'
                build_mgfs_from_mzxml(xml, outfile, self.temp,
                                      remove_z_one, split_2_3, add_hcd)
                if not self.keep:
                    os.remove(os.path.join(self.temp, xml))
#
#
#
class ReAdW4Mascot2():
    
    EXE = "exe_readw4mascot2/ReAdw4Mascot2.exe"
    
    def __init__(self, panel, logger):
        self.panel = panel
        self.logger = logger if logger else sys.stdout
        self.temp = 'readw4mascot2_temp'
        self.keep_format = False
        self.mgf = []
        self.get_options()
    
    def get_options(self):
        """Options (-option)
        c =           'Centroid the data (meaningfull only if RAW data is profile)'
        XmlOrbiMs1Profile='Save Profile FTMS ms1 spectra (override -c) into mzXml'
        Compression = 'Use zlib for comrpessing peaks in mzXML.',
        Rulers =      'Attempt compression using m/z ruler.',
        ms1 =         'Also output MS1 spectra and precursor scan no. for MS2.',
        sep1 =        'Output MS1 spectra in a separate outfile.RAW.MS1',
        metadata =    'Output Instrument Method, Tune Data, and scan Trailer info.',
        NoMgf =       'Do not produce MGF output.',
        NoMzXml =     'Do not produce MzXml output.',
        ChargeMgfOrbi='Include CHARGE in MGF output for LTQ Orbitrap instruments.',
        MonoisoMgfOrbi='In MGF, PEPMASS=Monoisotopic m/z for LTQ Orbitrap instruments',
        MonoisoMgf =  'In MGF, PEPMASS=Monoisotopic m/z if available',
        FixPepmass =  'Replace PEPMASS and CHARGE with better ones if found (Orbitrap and FT only)',
        MaxPI =       'Include max precursor intensity',
        PIvsRT_Debug= 'Include precursor intensity vs RT lines, verbose',
        PIvsRT =      'Include precursor intensity vs RT lines',
        SampleInfo =  'Output Sample Info into MGF',
        NoPeaks =     'Do not output mass spectral peaks',
        NoPeaks1 =    'Output ms1 spectra without mass spectral peaks',
        xpw width =   'width = Min precursor XIC Peak Width (seconds); default=0.0\n',
                      '(max. moving average window for preliminary ms1 XIC peak detection) = 0.5 * (width)'),
        xpm points =  'points = min. number of ms1 XIC points per minute >19 for condensing\n'
                      'the data, e.g. 32 means 104 points per 3.25 min. default=no condensing'),
        AutoOrbi =    'automatically pick the right options based on instrument.',
        output =      'output path. Path to the output folder\n'
                      'if not supplied, the raw file folder is used\n'
                      'output file name is created by adding extension .mzXML.')
        """
        
        options = []
                    
        for option, values in self.panel.readw4mascot2.hints.iteritems():
            
            name = values[0]
            
            try:
                widget = getattr(self.panel.readw4mascot2, name)
            except AttributeError:
                continue
            
            if name.startswith('ckbx'):
                if widget.IsChecked(): options.append('-%s' %option)
            elif name.startswith('tc'):
                value = widget.GetValue()
                if value: options.append('-%s %s' % (option, value))
            else:
                value = widget.GetValue()
                setattr(self, option, value)
                
        self.options = ' '.join(options)
            
    def execute(self):
        "call converter executable"
        if not os.path.exists(self.temp):
            os.mkdir(self.temp)
            
        for raw in self.panel.infiles:
            command = "%s %s %s %s" % (self.EXE, self.options, raw, self.temp)
            self.logger.write(command + '\n')
            process = subprocess.Popen(command,
                                             stdout=subprocess.PIPE,
                                             stderr=subprocess.PIPE,
                                             shell= False)
                                            
            self.logger.write(process.stdout.read())
            self.logger.write(process.stderr.read())
            
    def postprocess(self, remove_z_one, split_2_3, add_hcd):
        "converts MGF from ReAdW4mascot2 in custom mgfs"
        
        for mgf in os.listdir(self.temp):
            if mgf.endswith('MGF'):
                if mgf[:4] in ('ms2_', 'ms3_', 
                                '1ms2', '1ms3', '2ms2', 
                                '2ms3', 'ms23', 'out_'): continue
                self.mgfname = mgf
                self.fpath = os.path.join(self.temp, self.mgfname)
                self.get_spectra_lines()
                
                if add_hcd: self.add_hcd_and_cid()
                
                self.save_mgfs(remove_z_one, split_2_3)
        
    
    def get_spectra_lines(self):
        "Converts a file with espectra in a list of list of lines of spectra."
        BEGIN = 'BEGIN IONS\n'
        text = open(self.fpath).read()
        datalist = text.split(BEGIN)
        self.list_mgf_lines = []

        for espectro in datalist[1:]:
            lines = espectro.split('\n')
            self.list_mgf_lines.append(lines)
        
    
    def add_hcd_and_cid(self):
        "Mix hcd scan to next cid scan when they have same parent mass."
        hay_hcd = False
        hcd_mix = []
       
        for mgf_lines in self.list_mgf_lines:
            pepmass = mgf_lines[1]
            spec_data = mgf_lines[:]
             
            if '@hcd' in mgf_lines[0]:
                hcd_mass = pepmass
                hcd_data = spec_data
                hay_hcd = True
                continue
            #print hcd_data
            #return
            if hay_hcd and (hcd_mass == pepmass):
                mix = add_quant_to_cid(hcd_data, spec_data, 2)
                hcd_mix.append(mix)
                hay_hcd = False
            elif hay_hcd:
                hcd_mix.append(hcd_data)
                hcd_mix.append(spec_data)
                hay_hcd = False
            else:                               #ms3 or other third scan
                hcd_mix.append(spec_data)
                
        self.list_mgf_lines = hcd_mix
                    
                    
    def save_mgfs(self, remove_z_one, split_2_3):
        """Build an save mgfs in corresponding file.
        
        Converts mzXML header:
        
        #INPUT FILE=test_Fosfomix.RAW
        #OPTIONS= -NoMzXml -MonoisoMgfOrbi
        #INFO= InstrumentModel:"LTQ Orbitrap XL" RawFileSoftwareVersion:"2.4 SP2" ConverterVersion:"20091027a"
        BEGIN IONS
        TITLE=Scan:2 RT:10.012 nMSN:1 PrecursorMonoisoMZ:574.7148 PEPMASS:Monoiso
              PrecursorMZ:574.0000 PrecursorCharge:2 PrecursorScanFTMS:1 IBP:245.40
              ITot:3132.58 max2med:50.25 InjTime:200.00 IsolationMZ:574.7222
              PrecursorAb:0.00 MPY:1.00 ms1PrecursorTotAb:6416700.89 ms1PrecursorInjTime:63.38
              ms1PrecursorMZ:574.7148 ms1PrecursorMzAvg:574.7148 ms1PrecursorMzRMS:0.0000
              ms1PrecursorIntens:4709.14 ms1PrecursorRT:10.006 ms2IsolationWidth:3.00
              NumPeaks:298 Filter:ITMS + c NSI d Full ms2 574,72@cid30,00 [145,00-1160,00]
        PEPMASS=574.7148
        
        in custom mgf header:
    
        BEGIN IONS
        TITLE=spot_M1009#_mioblasts_DIGE,Scan:1012,MS:2,Rt:1833.494
        PEPMASS=419.32
        SCANS=1012
        RTINSECONDS=1833.494
        """
        
        pattern1 = "TITLE=Scan:(\d*) RT:(\d*.\d*) .*PrecursorCharge:(\d) .*Full ms(\d)"
        #pattern2 = "TITLE=Scan:(\d*) .*Full ms(\d)"    #error?
        pattern2 = "TITLE=Scan:(\d*) RT:(\d*.\d*) .*Full ms(\d)" 
        
        #self.mgfname = test_Fosfomix.RAW.MGF
        (path, ext) = os.path.splitext(self.mgfname)
        (mgfbasename, _) = path.rsplit('.')
        
        mgfname = mgfbasename + ext.lower()
        
        fhndls = get_file_handles(self.temp,
                                  mgfname, split_2_3, remove_z_one)
     
        if split_2_3 and remove_z_one:
            (fh12, fh13, fh2, fh3) = fhndls
        elif split_2_3:
            (fh2, fh3) = fhndls
        elif remove_z_one:
            (fh123, fh231) = fhndls
        else:
            (fh,) = fhndls
            
        #build_mgf(lines, mass, charge, rawname, rt, ms, scans, mz=None)
        for mgf_lines in self.list_mgf_lines:
            match = re.match(pattern1, mgf_lines[0])
            if match:
                #print '***ok***'
                (scans, rt, charge, ms) = match.groups()
            else:
                #print '**fail**'
                match = re.match(pattern2, mgf_lines[0])
                (scans, rt, ms) = match.groups()
                charge = '0'   
            #'PEPMASS=574.7148'
            mz = mgf_lines[1].split('=')[1]
            
            if not self.keep_format:
                #print 'change format'
                mgf_lines = build_mgf(mgf_lines[1:], None,
                                      charge, mgfbasename, rt, ms, scans, mz)
                mgf_lines[0] = mgf_lines[0].rstrip()
                espectro = '\n'.join(mgf_lines)
            else:
                espectro = 'BEGIN IONS\n' + '\n'.join(mgf_lines)
            
            if split_2_3 and remove_z_one:
                if charge == '1':
                    if ms == '2':
                        fh12.write(espectro)
                    elif ms == '3':
                        fh13.write(espectro)
                else:
                    if ms == '2':
                        fh2.write(espectro)
                    elif ms == '3':
                        fh3.write(espectro)
            elif split_2_3:
                if ms == '2':
                    fh2.write(espectro)
                elif ms == '3':
                    fh3.write(espectro)
            elif remove_z_one:
                if charge == '1':
                    fh123.write(espectro)
                else:
                    fh231.write(espectro)
            else:
                fh.write(espectro)
                    
        for hndl in fhndls: hndl.close()
#
#
#
class ExtractMsn():
    
    def __init__(self, child, panel, temp, logger):
        self.child = child
        self.panel = panel
        self.temp = temp
        self.logger = logger if logger else sys.stdout
        self.keep_dtas = False
        self.get_options()
        
    def get_options(self):
        """Get options from notebook sheet.
        
        extract_msn usage:  extract_msn [options] [datafile]
        options =
        -Fnum     where num is an INT specifying the first scan
        -Lnum     where num is an INT specifying the last scan
        -Bnum     where num is a FLOAT specifying the bottom MW for datafile creation
        -Tnum     where num is a FLOAT specifying the top MW for datafile creation
        -Mnum     where num is a FLOAT specifying the precursor mass
                   tolerance for grouping (default=1.4)
        -Snum     where num is an INT specifying the number of allowed
                   different intermediate scans for grouping. (default=1)
        -Cnum     where num is an INT specifying the charge state to use
        -Gnum     where num is an INT specifying the minimum # of related
                   grouped scans needed for a .dta file (default=2)
        -Inum     where num is an INT specifying the minimum # of ions
                   needed for a .dta file (default=0)
        -Dstring  where string is a path name
        -Enum     where num is FLOAT specifying the intensity threshold
        -U        Use a unified search file
        -Ystring  where string is a subsequence
        -Z        Controls whether the zta files are written
        -K        Controls whether the charge calculations are performed
        -Ostring  where string is the path of a template file
                   [Default name is chgstate.tpl]
                
        # -A only for msn_com_classic (vers 4)
        -Astring  where the string can contain any of the options
                   T: use template          F: use discrete Fourier transform
                   E: use Eng's algorithm   H: use scan header
                   M: use MSMS count
                   O: override header charge state
                   S: create summary file   L: create log file
                   D: create both files     C: create MSMS count file
                   A: find CS even for nonzero headers
                   tfehm: include algorithm output in summary file even if not called
                   [NOTE: This version of the program has a default string of -AHTFEMAOSC,
                   but if -A option is used all desired parameters must be specified]
        -H        print this information

        -Rnum     where num is a FLOAT specifying the minimum signal-to-noise value
                   needed for a peak to be written to a .dta file (default=3)
        -rnum     where num is an INT specifying the minimum number of major peaks
                   (peaks above S/N threshold) needed for a .dta file (default=5)

        If lcq_dta.exclude present, will ignore list of ions in exclude list.
        Format of lcq_dta.exclude:  mass tolerance on 1st line
                                    precursor masses on subsequent lines
        """
        
        options = []
        
        child = getattr(self.panel, self.child)
        childhints = getattr(child, 'hints')
                    
        for option, values in childhints.iteritems():
           
            name = values[0]
            
            try:
                widget = getattr(child, name)
            except AttributeError:
                print 'error'
                continue
            
            if name.startswith('ckbx'):
                if widget.IsChecked(): options.append('-%s' %option)
            elif name.startswith('tc'):
                value = widget.GetValue()
                if value:
                    options.append('-%s%s' %(option, value))
            else:
                value = widget.GetValue()
                setattr(self, option, value)
                   
                
        self.options = ' '.join(options)
        
        
    def execute(self):
        "call converter executable"
        if not os.path.exists(self.temp):
            os.mkdir(self.temp)
            
        for raw in self.panel.infiles:
            rawname = os.path.basename(raw)
            (rawname, ext) = os.path.splitext(rawname)
            self.dta_dir = os.path.join(self.temp, rawname)
            if not os.path.exists(self.dta_dir):
                os.mkdir(self.dta_dir)
            options = self.options + ' -D%s' % self.dta_dir
            command = "%s %s %s" % (self.EXE, options, raw)
            
            self.logger.write(command + '\n')
            #original
            process = subprocess.Popen(command,
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,
                                        shell=False)
            #desaparece la shell pero sigue sin funcionar el segundo run 
            #process = subprocess.Popen(command, close_fds=True, shell=True)
        
            (out, err) = process.communicate()
            self.logger.write(rawname + '\n')
         
            
    def postprocess(self, **kargs):
        ""
        remove_z_one = kargs['remove_z_one']
        split_2_3 = kargs['split_2_3']
        add_hcd = kargs['add_hcd']
        
        for raw in self.panel.infiles:
            rawname = os.path.basename(raw)
            (rawname, ext) = os.path.splitext(rawname)
            self.dta_dir = os.path.join(self.temp, rawname)
            outfile = os.path.join(self.temp, rawname + '.mgf')
            build_mgf_from_dtas(self.dta_dir, raw, 
                                outfile, remove_z_one,
                                split_2_3, add_hcd)
            
            if not self.keep_dtas:
                for arch in os.listdir(self.dta_dir):
                    if arch.endswith('.dta'):
                        os.remove(os.path.join(self.dta_dir, arch))                     
#
#
#
class ExtractMsnClassic(ExtractMsn):
    
    EXE = "exe_extract_msn/extract_msn.exe"
    
    def __init__(self, panel, logger):
        temp = 'extract_msn_classic_temp'
        ExtractMsn.__init__(self, 'extract_msn_classic', panel, temp, logger)
#
#
#
class ExtractMsnCom(ExtractMsn):
    
    EXE = "exe_extract_msn_com/extract_msn_com.exe"
    
    def __init__(self, panel, logger):
        temp = 'extract_msn_com_temp'
        ExtractMsn.__init__(self, 'extract_msn_com', panel, temp, logger)
#
#
#
class Easier(MyEasierFrame, Literate):
    ""
    hints = dict(listbox =  ('lbx_files',
                             ('Raw files to convert.\n'
                              'Select with buttons or Drag and Drop')),
                 scan_sequence = ('lb_scanmode',
                                  ('Sequence of scans performed\n\n'
                                  'Only HCD/PQD settings are funtional')),
                 hcd_cid_label = ('lb_pqd',
                                  ('number of pqd or hcd scans.\n\n'
                                   'set >0 for mixing hcd or pqd to following\n'
                                   'scan with same precursor mass.\n\n'
                                   'No other function available')),
                 hcd_cid_spin = ('sp_pqd',
                                 ('number of pqd or hcd scans.\n\n'
                                  'set >0 for mixing hcd or pqd to following\n'
                                  'scan with same precursor mass.\n\n'
                                  'No other function available'),
                                  1),
                 cid = ('lb_cid',
                        'No Functional!  '),
                 ms3 = ('lb_ms3',
                        'No Functional!  '),
                 remove_1 = ('ckbx_remove1',
                              'remove ms2 spectra from precursor with charge +1',
                              1),
                 split23 = ('ckbx_split23',
                            'save ms2 and ms3 scan in different mgf file',
                            1)
                )
    
    engynes = {0:ReAdW4Mascot2, 1:ExtractMsnClassic,
               2:ExtractMsnCom, 3:ReAdW}
    
    def __init__(self, parent):
        ""
        MyEasierFrame.__init__(self, None)
        Literate.__init__(self, self.hints)
        self.defdir = os.getcwd()
        self.infiles = []
        self.logger = TextCtrlLogger(self.tc_log)
        
        dt1 = MyFileDropTarget(self)
        self.lbx_files.SetDropTarget(dt1)
        
        self.Bind(wx.EVT_BUTTON, self.on_run, self.bt_run)
        self.Bind(wx.EVT_BUTTON, self.on_select_files, self.bt_sel)
        self.Bind(wx.EVT_BUTTON, self.on_delete_files, self.bt_unsel)
        self.Bind(wx.EVT_BUTTON, self.on_clear_files, self.bt_clear)        
        
    def on_run(self, event):
        "run conversion calling the class given by the selected notebook sheet"
        add_hcd = bool(self.sp_pqd.GetValue())
        remove_z_one = self.ckbx_remove1.IsChecked()
        split_2_3 = self.ckbx_split23.IsChecked()
        panel = self.nbook.GetSelection()
        Klass = Easier.engynes[panel]
        
        if self.infiles:
            worker = Klass(self, self.logger)
            worker.execute()
            worker.postprocess(remove_z_one=remove_z_one,
                               split_2_3=split_2_3,
                               add_hcd=add_hcd)
        
        self.logger.write('terminated\n')
         
    def on_select_files(self, event):
        """dialog for selecting files. Updates listbox"""
        
        flpth = None
        #
        fd = wx.FileDialog(None, defaultDir=self.defdir, style=wx.FD_MULTIPLE)
        #
        if fd.ShowModal() == wx.ID_OK:
            flpth = fd.GetPaths()
        #
        if not flpth: return
        
        self.infiles.extend(flpth)
        self.lbx_files.InsertItems(flpth, 0)
        
    def on_delete_files(self, event):
        "delete selected files from listbox"
        selections = self.lbx_files.GetSelections()
        for item in selections[::-1]:
            self.lbx_files.Delete(item)
            self.infiles.remove(self.infiles[item])
            
    def on_clear_files(self, event):
        "delete all files from listbox"
        self.lbx_files.Clear()
        self.infiles = []
            
    def notify(self, files):
        "Update file listbox after drag and drop"
        self.infiles.extend(files)
        self.lbx_files.InsertItems(files, 0)


if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = Easier(None)
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()